fix(security): gate control-plane writes, hard-deny lifecycle files - #617
Merged
Conversation
An agent session edited ~/.netclaw/config/netclaw.json via file_write; the config watcher detected the change and triggered a full daemon restart, dropping the session mid-turn with no warning or attribution. ToolPathPolicy only protected secrets.json, webhooks/, and keys/ — the rest of the control plane was writable by any tool with the `file` grant, and Personal audience has no root-containment, so the deny list was the entire security boundary. Split ToolPathPolicy into three independent enforcement surfaces: write deny, read deny, and a narrow shell indicator list (so directory-scoped write-deny entries don't bleed into the substring scan that gates `shell_execute` — otherwise `ls ~/.netclaw/config` would regress). Tier 1 (hard deny, no approval possible): secrets, keys, netclaw.db, pid/lock files, restart-manifest.json. Corrupting these is unrecoverable and no legitimate agent flow edits them. Tier 2 (approval-gated via FilePathApprovalMatcher): everything under ~/.netclaw/config/ that isn't Tier 1 — netclaw.json, devices.json, tool-approvals.json, mcp-oauth-metadata.json, webhooks/*.json. The matcher inspects the target path and routes control-plane writes to a `file_write:control-plane` approval-mode key with per-path patterns so approving netclaw.json doesn't implicitly approve tool-approvals.json. Fail-closed default in GetMissingApprovalPolicyDefaultMode: when ApprovalPolicy is null on Personal audience, control-plane writes still require interactive approval, matching the existing fail-closed behavior for shell_execute. FileReadTool switches from IsDenied to the narrower IsReadDenied so the agent can still read netclaw.json for diagnostics while writes are gated. Error messages on all three file tools now name the offending path and point at the right escape hatch (`netclaw doctor --fix`, `netclaw secrets set`) instead of a generic "protected by security policy" string.
…ogic Review followups on the control-plane write fix: - Extract duplicated control-plane deny message from FileWriteTool and FileEditTool into FileToolErrors so the copy can't drift. - Move the "does this invocation require fail-closed approval?" decision onto IToolApprovalMatcher (new IsFailClosedOnPersonal method) instead of inspecting approval-mode-key string suffixes from ToolAccessPolicy. The shell fail-closed default now lives in ShellApprovalMatcher where it belongs; FilePathApprovalMatcher answers the same question for control-plane paths without ToolAccessPolicy needing to know its key format. - Drop unused ControlPlanePatternPrefix const and narrative/incident comments that referenced the originating fix rather than the invariant.
Ensure matcher and base-key precedence plus approve-once retries use the same filtered pattern set so approval prompts stay deterministic. Harden shell resource deny coverage and sync/archive the related OpenSpec updates.
Aaronontheweb
marked this pull request as ready for review
April 12, 2026 23:32
Aaronontheweb
enabled auto-merge (squash)
April 12, 2026 23:32
This was referenced Apr 13, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
~/.netclaw/config/netclaw.jsonand other control-plane files, which can trigger daemon restart and drop the active session mid-turn.ToolPathPolicyinto three independent deny surfaces: write-deny (Tier 1 hard deny for secrets, keys, SQLite DB, pid/lock/restart-manifest), read-deny (credentials only), and shell-indicators (narrow enough to keep diagnostics likels ~/.netclaw/configworking).FilePathApprovalMatcherfor argument-aware control-plane write/edit approvals, with path-scoped patterns likefile_write:control-plane:netclaw.json.netclaw.db,netclaw.pid,netclaw.lock,cache/restart-manifest.json) while keeping operation hard-deny precedence.Behavior changes
shell_execute: ls ~/.netclaw/configrisked collateral blocking with broad indicatorsFiles
src/Netclaw.Security/ToolPathPolicy.cs- deny surface refactor and shell indicator behaviorsrc/Netclaw.Security/IToolApprovalMatcher.cs- matcher extension points used for path-aware mode keys/fail-closedsrc/Netclaw.Actors/Tools/FilePathApprovalMatcher.cs- control-plane path-aware matchersrc/Netclaw.Actors/Tools/ToolAccessPolicy.cs- approval mode resolution precedence and matcher wiringsrc/Netclaw.Actors/Tools/DispatchingToolExecutor.cs- filtered unapproved-set approve-once matchingsrc/Netclaw.Actors/Tools/File{Write,Edit,Read}Tool.cs+FileToolErrors.cs- control-plane/credential deny behavior and copysrc/Netclaw.Daemon/Program.cs- deny-list and matcher composition wiringdocs/runbooks/tool-approval-gates.md- layered policy model + path-aware approval docssrc/Netclaw.Configuration/Schemas/netclaw-config.v1.schema.json- matcher-specific override key docsOpenSpec
openspec/specs/tool-approval-gates/spec.mdopenspec/changes/archive/2026-04-12-tool-approval-composition-fixes/Test plan
dotnet test src/Netclaw.Actors.Tests/ --filter "ToolApprovalGateTests|DispatchingToolExecutorTests|SessionToolExecutionPipelineTests|ShellToolTests"(51/51)dotnet test src/Netclaw.Security.Tests/ --filter "ToolPathPolicyTests|ShellCommandPolicyTests"(60/60)dotnet test Netclaw.slnx(full suite green)dotnet slopwatch analyze(0 issues)file_writeagainst~/.netclaw/config/netclaw.json-> approval prompt appears with control-plane pattern; declined -> session survives; approved -> write proceedsshell_execute: ls ~/.netclaw/configstill succeedsfile_writeagainst~/.netclaw/netclaw.db-> immediate fail-loud error, no approval prompt